AWS SDK の ClientSideMonitoring について調べてみた
背景
https://github.com/iann0036/iamlive は AWS の API を読み取って動的に IAM を生成してくれる大変素晴らしいツールです。
iamlive ではモードは 2 種類あって、proxy として読み取るパターンと CSM を使って読み取るパターンがあります。
このツールを参考に CSM を取得して既存のスクリプトの挙動を理解するのに役立つツールを作れればと思い、コードを読んでみました。
CSM とは
python の AWS SDK 実装である boto では 実行した内容と実行結果をエージェントに送信する機構が追加されています。
https://github.com/boto/botocore/commit/14e0eab5c1e4aec437c3e558e6899de00fd5e98e
環境変数 AWS_CSM_ENABLED=true
を設定すること, もしくは csm_enabled = true
を AWS profile に記載することで有効になります。
全ての SDK が対応している訳ではなく、たとえば Go の AWS SDK v2 はユースケースの見直しから行われているようです。
https://github.com/aws/aws-sdk-go-v2/issues/1142
やってみた
ここのコードが CSM agent の全貌となっているようでした。
https://github.com/iann0036/iamlive/blob/main/iamlivecore/csm.go#L151-L204
- 31000 ポートがデフォルトであること
- UDP でやりとりしていること
- 受け取るデータが JSON フォーマットであること
上記が agent 側で受け取るための要件のようです。
そのため、下記のようなスクリプトで実際のデータを確認してみました。
package main import ( "bytes" "flag" "fmt" "io" "log" "net" "os" ) func main() { var ( host string port int ) flag.StringVar(&host, "host", "localhost", "") flag.IntVar(&port, "port", 31000, "") flag.Parse() if err := listenForEvents(host, port, os.Stdout); err != nil { log.Fatal(err) } } func listenForEvents(addr string, port int, w io.Writer) error { conn, err := net.ListenUDP("udp", &net.UDPAddr{ IP: net.ParseIP(addr), Port: port, }) if err != nil { return fmt.Errorf("failed to start server: %w", err) } defer conn.Close() if err := conn.SetReadBuffer(1024 * 1024); err != nil { return fmt.Errorf("failed to set buffer: %w", err) } var buf [1024 * 1024]byte for { rlen, _, err := conn.ReadFromUDP(buf[:]) if err != nil { return fmt.Errorf("failed to read data: %w", err) } if _, err := io.Copy(w, bytes.NewReader(buf[:rlen])); err != nil { return fmt.Errorf("failed to write data: %w", err) } if _, err := w.Write([]byte("\n")); err != nil { return fmt.Errorf("failed to write data: %w", err) } } }
では 実際に aws s3 ls
を実行して確認してみます。
$ export AWS_CSM_ENABLED=true $ aws s3 ls An error occurred (ExpiredToken) when calling the ListBuckets operation: The provided token has expired.
$ go run . | jq . { "Version": 1, "ClientId": "", "Type": "ApiCallAttempt", "Service": "S3", "Api": "ListBuckets", "Timestamp": 1657606793456, "AttemptLatency": 878, "Fqdn": "s3.us-east-2.amazonaws.com", "UserAgent": "aws-cli/2.7.14 Python/3.10.5 Darwin/21.5.0 source/x86_64 prompt/off command/s3.ls", "AccessKey": "ASIAXXXXXXXXXXXXXXXX", "Region": "us-east-2", "SessionToken": "xxx", "HttpStatusCode": 400, "XAmzRequestId": "1X9N58VBR4VQHD7V", "XAmzId2": "VCsV9PkopYZPkU3oU3iDjSlFcjZf1KpygItV3UofMMi6j8MrNy6cW6HPoWdyACR4hmqRgjMbrEY=", "AwsException": "ExpiredToken", "AwsExceptionMessage": "The provided token has expired." } { "Version": 1, "ClientId": "", "Type": "ApiCall", "Service": "S3", "Api": "ListBuckets", "Timestamp": 1657606793455, "AttemptCount": 1, "Region": "us-east-2", "UserAgent": "aws-cli/2.7.14 Python/3.10.5 Darwin/21.5.0 source/x86_64 prompt/off command/s3.ls", "FinalHttpStatusCode": 400, "FinalAwsException": "ExpiredToken", "FinalAwsExceptionMessage": "The provided token has expired.", "Latency": 880, "MaxRetriesExceeded": 0 }
有効期限が切れたまま実行してしまいましたが、ログとしては残るようです。 ここらへんのログは CloudTrail にはないログという認識です。
$ eval "$(mfa gen aws | assume-profile --profile classmethod-dev)" $ aws s3 ls 2020-03-30 10:21:11 xxx-xxxxxx-xxxxx-xxxxxxx-000000000000-xx-xxxxxxxxx-0 2021-09-10 14:33:16 xxx-xxx-xxx-xxxxxxx-xxxxxxx-xxxxxxxxxxxxxxxxxx-00xx0x00xxxx0 2021-11-18 10:23:17 xxx-xxx-xxx-xxxxxxx-xxxxxxx-xxxxxxxxxxxxxxxxxx-0x0xxxxxxxxx 2022-01-24 13:47:48 xxx-xxx000xxx-xxxxxx-000000000000-xx-xxxx-0 2020-06-18 23:42:04 xxxxxxxxxx-xxxxxxxxxxxxx-00xx0xxxxxxx0 2022-01-24 13:48:04 xxxxxxxxxx-xxxxxxxxxxxxx-xxxxx0x0xx00 2020-08-12 22:49:29 xxxxxxxxxx-xxxxxxxxxxxxx-xxxx0x00xxxx 2022-04-11 15:46:01 xx-xxxxxxxxx-xxx0x0xxxxxx-xx-xxxxxxxxx-0 2020-01-24 13:47:39 xx-xxxxxxx-000000000000 2021-04-27 13:09:16 xxxx-xxxxxxxxx-000000000000-xx-xxxxxxxxx-0 2021-04-27 11:48:30 xxxx-xxxxxxxxx-000000000000-xx-xxxx-0
実際の実行ログを取得できました。
{ "Version": 1, "ClientId": "", "Type": "ApiCallAttempt", "Service": "S3", "Api": "ListBuckets", "Timestamp": 1657606929271, "AttemptLatency": 980, "Fqdn": "s3.us-east-2.amazonaws.com", "UserAgent": "aws-cli/2.7.14 Python/3.10.5 Darwin/21.5.0 source/x86_64 prompt/off command/s3.ls", "AccessKey": "ASIAXXXXXXXXXXXXXXXX", "Region": "us-east-2", "SessionToken": "xxx", "HttpStatusCode": 200, "XAmzRequestId": "VDKP9G5VJF9NYBM7", "XAmzId2": "4Ke5/rdd3U/ZX7Y3tzRSD4TN2GVMzTVadiUNN/1z8WcujWKRKQ/VT/wtz1p3aweN8pvpJGpkKnQ=" } { "Version": 1, "ClientId": "", "Type": "ApiCall", "Service": "S3", "Api": "ListBuckets", "Timestamp": 1657606929270, "AttemptCount": 1, "Region": "us-east-2", "UserAgent": "aws-cli/2.7.14 Python/3.10.5 Darwin/21.5.0 source/x86_64 prompt/off command/s3.ls", "FinalHttpStatusCode": 200, "Latency": 982, "MaxRetriesExceeded": 0 }
まとめ
環境変数を設定するだけでCSMの送信が簡単にできました。またagent側も特に難しいコードは不要で実装できました。 一方で取得できる値もあくまでエラーやパフォーマンスで使用できることを目的としているように見受けられ、 今回の背景にあった既存スクリプトの概要をつかむ、といった内容には向いていませんでした。
参考サイトにもあったとおり、CloudTrail のほうがパラメータが含まれている点などでログは充実しており、またSDKの実装に依存しないため、 ユースケースによっては参照するログを使い分ける必要がありそうです。